【API Gatewayタイムアウト対策】Step Functionsを組み合わせて非同期処理にしてみる
どうも!大阪オフィスの西村祐二です。
下記図のような、webサイト上のボタンをクリックし、API Gatewayを経由しLambdaで処理を実行し、その処理結果をブラウザに表示するといった構成はよくあるパターンかと思います。
このような構成で躓くところとして、API Gatewayのタイムアウト制限があります。遭遇場面としてはLambdaで時間のかかる処理をしたときに遭遇することがあります。現状(2018/7/3時点)、API Gatewayは最大29秒でタイムアウトする制限となっています。つまり、29秒以内にレスポンスを返し、裏ではLambdaが処理を実行し続けるような非同期の形にする必要があります。
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/limits.html
私もちょうどこの問題に直面し、なるべく簡単に非同期にできないかなと考えていたところStep Functionsを組み合わせることで簡単に非同期にできたので、今回紹介したいと思います。
やりたいこと
構成としては、API GatewayとLambdaの間にStep Functionsを入れ、Step FunctionsからLambdaを実行する構成となります。
簡単なシーケンスは下記になります。
ポイント:StartExecution APIはタスク実行後、実行状況に関係なくタスク名をレスポンスとして返してくれる
API GatewayからStep FunctionsのStartExecution APIを呼び出すのですが、このAPIを呼び出すと、Step Functionsで設定したLambdaのステートマシンでタスクを実行し、レスポンスとして実行したタスク名をAPI Gatewayに返します。API Gatewayとしてはレスポンスが返ってくるので、タイムアウトの制限にひっかかることはなくなり、裏ではLambdaが実行され非同期処理の形にすることができます。
https://docs.aws.amazon.com/ja_jp/step-functions/latest/apireference/API_StartExecution.html
ポイント:DescribeExecution APIはLambdaの処理結果をOutput属性に含めてレスポンスを返してくれる
クライアント側ではLambdaの処理が完了したら、処理結果を取得し画面表示したいので、ポーリングする形でステートマシンのタスク状況を取得します。タスクの実行状況を取得するためにAPI GatewayからStep FunctionsのDescribeExecution APIを呼び出します。このAPIの嬉しいところは、タスクの実行状況だけでなく、処理が完了していたら処理結果をOutput属性として追加してくれるところです。つまり、このAPIだけでタスク状況と処理結果一緒に取得することができます。これにより、クライアント側の実装コストを削減することができます。
https://docs.aws.amazon.com/ja_jp/step-functions/latest/apireference/API_DescribeExecution.html
実際にやってみる
Lambda関数を作成
下記設定でLambda関数を作成します。処理内容としては、30秒経過したら、メッセージを返す処理としています。
- ランタイム:Python3.6
- 関数名:test-lambda
- タイムアウト:1分
from time import sleep def lambda_handler(event, context): # Timer SEC = 30 sleep(SEC) data = {"message": "One minute elapsed"} return data
Step FunctionsでLambda ステートマシンを作成
ステートマシンを作成していきます。名前をtest-state
としています。内容は単純にLambdaを呼び出しているだけです。
ステートマシンの定義は下記になります。検証が目的なのでシンプルなものにしています。
Resource
には上記で作成したLambdaを指定します。
{ "StartAt": "Test", "States": { "Test": { "Type": "Task", "Resource": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:test-lambda", "End": true } } }
API GatewayでStep Functionsと連携するAPIを作成
API GatewayからStep FunctionsのAPIを呼び出すようにします。
下記ドキュメントを参考に作成しています。IAMロールも下記ドキュメントを参考に作成しておいてください。
https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-api-gateway.html
※CORSを忘れずに有効にしておいてください。
▼Step Functionsのステートマシンを実行できるAPIのStartExecution
を設定します。
▼実行タスクの状態を取得できるAPIのDescribeExecution
を設定します。
クライアント側の実装
Angularで実装してみます。
環境
- Angular CLI: 6.0.8
- Node: 9.6.1
- OS: darwin x64
実装
▼HTTPリクエストをするapi.service.ts
を実装します。
angularのhttpモジュールを利用するので、予めapp.module.ts
にHttpClientModule
をインポートしておいてください。
import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; const httpOptions = { headers: { 'Content-Type': 'application/json' } }; @Injectable({ providedIn: 'root' }) export class ApiService { url = 'https://xxxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/test/execution'; execTaskName = ''; constructor(private http: HttpClient) {} getRndStr(): String { // 使用文字の定義 const set_str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const len_str = 8; // ランダムな文字列の生成 let result = ''; for (let i = 0; i < len_str; i++) { result += set_str.charAt(Math.floor(Math.random() * set_str.length)); } return result; } exec_task(): Observable<any> { const reqbody = { input: `{"message": "clinet request"}`, name: 'MyExecution_' + this.getRndStr(), stateMachineArn: 'arn:aws:states:ap-northeast-1:<your account number>:stateMachine:test-state' }; return this.http.post<any>(this.url, reqbody, httpOptions); } status_task(): Observable<any> { const reqbody = { executionArn: this.execTaskName }; const statusUrl = this.url + '/status'; return this.http.post<any>(statusUrl, reqbody, httpOptions); } }
リクエストボディはAPIで定められている正しい値を設定してください。
https://docs.aws.amazon.com/ja_jp/step-functions/latest/apireference/API_StartExecution.html
https://docs.aws.amazon.com/ja_jp/step-functions/latest/apireference/API_DescribeExecution.html
▼api.service.tsを利用してコンポーネントを実装します。
import { ApiService } from './api.service'; import { Component, OnInit } from '@angular/core'; import { Subscription } from 'rxjs'; import { interval, timer } from 'rxjs'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { message = ''; private timer: Subscription; constructor(private api: ApiService) {} ngOnInit() {} getMessage() { this.api.exec_task().subscribe(result => { console.log(result); this.api.execTaskName = result['executionArn']; this.timer = interval(10000).subscribe(() => this.getStatus()); }); } getStatus() { this.api.status_task().subscribe(result => { console.log(result); if (Object.prototype.hasOwnProperty.call(result, 'output')) { const json_data = JSON.parse(result['output']); this.message = json_data['message']; this.timer.unsubscribe(); } }); } }
this.timer = interval(10000).subscribe(() => this.getStatus());
とすることで、10秒ごとにタスク状況を取得してくれます。output
の値は文字列で返ってくるので、JSON.parse
としてjsonに変換しています。output
が取得できた際にポーリングを止めるためにthis.timer.unsubscribe();
としています。
▼コンポーネントで実装した関数をボタンクリックで実行するようにします。
<button (click)="getMessage()">リクエスト</button> <h2 *ngIf="message">{{message}}</h2>
message
にはoutput
を取得できた段階で、値が格納され、画面に表示されます。
動作確認
実際の画面とボタンクリックした際のログは下記のようになります。
処理の流れは下記のとおりです。
- 画面上のボタンをクリックし、リクエスト
- レスポンスに含まれるタスク名を取得
- 取得したタスク名を使って10秒毎にタスク情報取得リクエスト
- レスポンスにOutputがあれば、表示し、ポーリング処理終了
- Outputがなれけば、タスク情報取得リクエストを繰り返す
さいごに
いかがだったでしょうか。
API Gatewayタイムアウト制限の対策として、Step Functionsを組み合わせて非同期処理にしてみました。
Lambdaのコードはあまり変えずに想定した動作になってくれたのでよかったです。
コンテナだとすこし大袈裟、LambdaからLambdaを呼び出す構成もなんだかなと感じている方は試してみていただければと思います。
誰かの参考になれば幸いです。